﻿/*
 * FCKeditor - The text editor for Internet - http://www.fckeditor.net
 * Copyright (C) 2003-2009 Frederico Caldeira Knabben
 *
 * == BEGIN LICENSE ==
 *
 * Licensed under the terms of any of the following licenses at your
 * choice:
 *
 *  - GNU General Public License Version 2 or later (the "GPL")
 *    http://www.gnu.org/licenses/gpl.html
 *
 *  - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
 *    http://www.gnu.org/licenses/lgpl.html
 *
 *  - Mozilla Public License Version 1.1 or later (the "MPL")
 *    http://www.mozilla.org/MPL/MPL-1.1.html
 *
 * == END LICENSE ==
 *
 * Class for working with a selection range, much like the W3C DOM Range, but
 * it is not intended to be an implementation of the W3C interface.
 */

var FCKDomRange = function( sourceWindow )
{
    this.Window = sourceWindow ;
    this._Cache = {} ;
}

FCKDomRange.prototype =
{

    _UpdateElementInfo : function()
    {
        var innerRange = this._Range ;

        if ( !innerRange )
            this.Release( true ) ;
        else
        {
            // For text nodes, the node itself is the StartNode.
            var eStart    = innerRange.startContainer ;

            var oElementPath = new FCKElementPath( eStart ) ;
            this.StartNode            = eStart.nodeType == 3 ? eStart : eStart.childNodes[ innerRange.startOffset ] ;
            this.StartContainer        = eStart ;
            this.StartBlock            = oElementPath.Block ;
            this.StartBlockLimit    = oElementPath.BlockLimit ;

            if ( innerRange.collapsed )
            {
                this.EndNode        = this.StartNode ;
                this.EndContainer    = this.StartContainer ;
                this.EndBlock        = this.StartBlock ;
                this.EndBlockLimit    = this.StartBlockLimit ;
            }
            else
            {
                var eEnd    = innerRange.endContainer ;

                if ( eStart != eEnd )
                    oElementPath = new FCKElementPath( eEnd ) ;

                // The innerRange.endContainer[ innerRange.endOffset ] is not
                // usually part of the range, but the marker for the range end. So,
                // let's get the previous available node as the real end.
                var eEndNode = eEnd ;
                if ( innerRange.endOffset == 0 )
                {
                    while ( eEndNode && !eEndNode.previousSibling )
                        eEndNode = eEndNode.parentNode ;

                    if ( eEndNode )
                        eEndNode = eEndNode.previousSibling ;
                }
                else if ( eEndNode.nodeType == 1 )
                    eEndNode = eEndNode.childNodes[ innerRange.endOffset - 1 ] ;

                this.EndNode            = eEndNode ;
                this.EndContainer        = eEnd ;
                this.EndBlock            = oElementPath.Block ;
                this.EndBlockLimit        = oElementPath.BlockLimit ;
            }
        }

        this._Cache = {} ;
    },

    CreateRange : function()
    {
        return new FCKW3CRange( this.Window.document ) ;
    },

    DeleteContents : function()
    {
        if ( this._Range )
        {
            this._Range.deleteContents() ;
            this._UpdateElementInfo() ;
        }
    },

    ExtractContents : function()
    {
        if ( this._Range )
        {
            var docFrag = this._Range.extractContents() ;
            this._UpdateElementInfo() ;
            return docFrag ;
        }
        return null ;
    },

    CheckIsCollapsed : function()
    {
        if ( this._Range )
            return this._Range.collapsed ;

        return false ;
    },

    Collapse : function( toStart )
    {
        if ( this._Range )
            this._Range.collapse( toStart ) ;

        this._UpdateElementInfo() ;
    },

    Clone : function()
    {
        var oClone = FCKTools.CloneObject( this ) ;

        if ( this._Range )
            oClone._Range = this._Range.cloneRange() ;

        return oClone ;
    },

    MoveToNodeContents : function( targetNode )
    {
        if ( !this._Range )
            this._Range = this.CreateRange() ;

        this._Range.selectNodeContents( targetNode ) ;

        this._UpdateElementInfo() ;
    },

    MoveToElementStart : function( targetElement )
    {
        this.SetStart(targetElement,1) ;
        this.SetEnd(targetElement,1) ;
    },

    // Moves to the first editing point inside a element. For example, in a
    // element tree like "<p><b><i></i></b> Text</p>", the start editing point
    // is "<p><b><i>^</i></b> Text</p>" (inside <i>).
    MoveToElementEditStart : function( targetElement )
    {
        var editableElement ;

        while ( targetElement && targetElement.nodeType == 1 )
        {
            if ( FCKDomTools.CheckIsEditable( targetElement ) )
                editableElement = targetElement ;
            else if ( editableElement )
                break ;        // If we already found an editable element, stop the loop.

            targetElement = targetElement.firstChild ;
        }

        if ( editableElement )
            this.MoveToElementStart( editableElement ) ;
    },

    InsertNode : function( node )
    {
        if ( this._Range )
            this._Range.insertNode( node ) ;
    },

    CheckIsEmpty : function()
    {
        if ( this.CheckIsCollapsed() )
            return true ;

        // Inserts the contents of the range in a div tag.
        var eToolDiv = this.Window.document.createElement( 'div' ) ;
        this._Range.cloneContents().AppendTo( eToolDiv ) ;

        FCKDomTools.TrimNode( eToolDiv ) ;

        return ( eToolDiv.innerHTML.length == 0 ) ;
    },

    /**
     * Checks if the start boundary of the current range is "visually" (like a
     * selection caret) at the beginning of the block. It means that some
     * things could be brefore the range, like spaces or empty inline elements,
     * but it would still be considered at the beginning of the block.
     */
    CheckStartOfBlock : function()
    {
        var cache = this._Cache ;
        var bIsStartOfBlock = cache.IsStartOfBlock ;

        if ( bIsStartOfBlock != undefined )
            return bIsStartOfBlock ;

        // Take the block reference.
        var block = this.StartBlock || this.StartBlockLimit ;

        var container    = this._Range.startContainer ;
        var offset        = this._Range.startOffset ;
        var currentNode ;

        if ( offset > 0 )
        {
            // First, check the start container. If it is a text node, get the
            // substring of the node value before the range offset.
            if ( container.nodeType == 3 )
            {
                var textValue = container.nodeValue.substr( 0, offset ).Trim() ;

                // If we have some text left in the container, we are not at
                // the end for the block.
                if ( textValue.length != 0 )
                    return cache.IsStartOfBlock = false ;
            }
            else
                currentNode = container.childNodes[ offset - 1 ] ;
        }

        // We'll not have a currentNode if the container was a text node, or
        // the offset is zero.
        if ( !currentNode )
            currentNode = FCKDomTools.GetPreviousSourceNode( container, true, null, block ) ;

        while ( currentNode )
        {
            switch ( currentNode.nodeType )
            {
                case 1 :
                    // It's not an inline element.
                    if ( !FCKListsLib.InlineChildReqElements[ currentNode.nodeName.toLowerCase() ] )
                        return cache.IsStartOfBlock = false ;

                    break ;

                case 3 :
                    // It's a text node with real text.
                    if ( currentNode.nodeValue.Trim().length > 0 )
                        return cache.IsStartOfBlock = false ;
            }

            currentNode = FCKDomTools.GetPreviousSourceNode( currentNode, false, null, block ) ;
        }

        return cache.IsStartOfBlock = true ;
    },

    /**
     * Checks if the end boundary of the current range is "visually" (like a
     * selection caret) at the end of the block. It means that some things
     * could be after the range, like spaces, empty inline elements, or a
     * single <br>, but it would still be considered at the end of the block.
     */
    CheckEndOfBlock : function( refreshSelection )
    {
        var isEndOfBlock = this._Cache.IsEndOfBlock ;

        if ( isEndOfBlock != undefined )
            return isEndOfBlock ;

        // Take the block reference.
        var block = this.EndBlock || this.EndBlockLimit ;

        var container    = this._Range.endContainer ;
        var offset            = this._Range.endOffset ;
        var currentNode ;

        // First, check the end container. If it is a text node, get the
        // substring of the node value after the range offset.
        if ( container.nodeType == 3 )
        {
            var textValue = container.nodeValue ;
            if ( offset < textValue.length )
            {
                textValue = textValue.substr( offset ) ;

                // If we have some text left in the container, we are not at
                // the end for the block.
                if ( textValue.Trim().length != 0 )
                    return this._Cache.IsEndOfBlock = false ;
            }
        }
        else
            currentNode = container.childNodes[ offset ] ;

        // We'll not have a currentNode if the container was a text node, of
        // the offset is out the container children limits (after it probably).
        if ( !currentNode )
            currentNode = FCKDomTools.GetNextSourceNode( container, true, null, block ) ;

        var hadBr = false ;

        while ( currentNode )
        {
            switch ( currentNode.nodeType )
            {
                case 1 :
                    var nodeName = currentNode.nodeName.toLowerCase() ;

                    // It's an inline element.
                    if ( FCKListsLib.InlineChildReqElements[ nodeName ] )
                        break ;

                    // It is the first <br> found.
                    if ( nodeName == 'br' && !hadBr )
                    {
                        hadBr = true ;
                        break ;
                    }

                    return this._Cache.IsEndOfBlock = false ;

                case 3 :
                    // It's a text node with real text.
                    if ( currentNode.nodeValue.Trim().length > 0 )
                        return this._Cache.IsEndOfBlock = false ;
            }

            currentNode = FCKDomTools.GetNextSourceNode( currentNode, false, null, block ) ;
        }

        if ( refreshSelection )
            this.Select() ;

        return this._Cache.IsEndOfBlock = true ;
    },

    // This is an "intrusive" way to create a bookmark. It includes <span> tags
    // in the range boundaries. The advantage of it is that it is possible to
    // handle DOM mutations when moving back to the bookmark.
    // Attention: the inclusion of nodes in the DOM is a design choice and
    // should not be changed as there are other points in the code that may be
    // using those nodes to perform operations. See GetBookmarkNode.
    // For performance, includeNodes=true if intended to SelectBookmark.
    CreateBookmark : function( includeNodes )
    {
        // Create the bookmark info (random IDs).
        var oBookmark =
        {
            StartId    : (new Date()).valueOf() + Math.floor(Math.random()*1000) + 'S',
            EndId    : (new Date()).valueOf() + Math.floor(Math.random()*1000) + 'E'
        } ;

        var oDoc = this.Window.document ;
        var eStartSpan ;
        var eEndSpan ;
        var oClone ;

        // For collapsed ranges, add just the start marker.
        if ( !this.CheckIsCollapsed() )
        {
            eEndSpan = oDoc.createElement( 'span' ) ;
            eEndSpan.style.display = 'none' ;
            eEndSpan.id = oBookmark.EndId ;
            eEndSpan.setAttribute( '_fck_bookmark', true ) ;

            // For IE, it must have something inside, otherwise it may be
            // removed during DOM operations.
//            if ( FCKBrowserInfo.IsIE )
                eEndSpan.innerHTML = '&nbsp;' ;

            oClone = this.Clone() ;
            oClone.Collapse( false ) ;
            oClone.InsertNode( eEndSpan ) ;
        }

        eStartSpan = oDoc.createElement( 'span' ) ;
        eStartSpan.style.display = 'none' ;
        eStartSpan.id = oBookmark.StartId ;
        eStartSpan.setAttribute( '_fck_bookmark', true ) ;

        // For IE, it must have something inside, otherwise it may be removed
        // during DOM operations.
//        if ( FCKBrowserInfo.IsIE )
            eStartSpan.innerHTML = '&nbsp;' ;

        oClone = this.Clone() ;
        oClone.Collapse( true ) ;
        oClone.InsertNode( eStartSpan ) ;

        if ( includeNodes )
        {
            oBookmark.StartNode = eStartSpan ;
            oBookmark.EndNode = eEndSpan ;
        }

        // Update the range position.
        if ( eEndSpan )
        {
            this.SetStart( eStartSpan, 4 ) ;
            this.SetEnd( eEndSpan, 3 ) ;
        }
        else
            this.MoveToPosition( eStartSpan, 4 ) ;

        return oBookmark ;
    },

    // This one should be a part of a hypothetic "bookmark" object.
    GetBookmarkNode : function( bookmark, start )
    {
        var doc = this.Window.document ;

        if ( start )
            return bookmark.StartNode || doc.getElementById( bookmark.StartId ) ;
        else
            return bookmark.EndNode || doc.getElementById( bookmark.EndId ) ;
    },

    MoveToBookmark : function( bookmark, preserveBookmark )
    {
        var eStartSpan    = this.GetBookmarkNode( bookmark, true ) ;
        var eEndSpan    = this.GetBookmarkNode( bookmark, false ) ;

        this.SetStart( eStartSpan, 3 ) ;

        if ( !preserveBookmark )
            FCKDomTools.RemoveNode( eStartSpan ) ;

        // If collapsed, the end span will not be available.
        if ( eEndSpan )
        {
            this.SetEnd( eEndSpan, 3 ) ;

            if ( !preserveBookmark )
                FCKDomTools.RemoveNode( eEndSpan ) ;
        }
        else
            this.Collapse( true ) ;

        this._UpdateElementInfo() ;
    },

    // Non-intrusive bookmark algorithm
    CreateBookmark2 : function()
    {
        // If there is no range then get out of here.
        // It happens on initial load in Safari #962 and if the editor it's hidden also in Firefox
        if ( ! this._Range )
            return { "Start" : 0, "End" : 0 } ;

        // First, we record down the offset values
        var bookmark =
        {
            "Start" : [ this._Range.startOffset ],
            "End" : [ this._Range.endOffset ]
        } ;
        // Since we're treating the document tree as normalized, we need to backtrack the text lengths
        // of previous text nodes into the offset value.
        var curStart = this._Range.startContainer.previousSibling ;
        var curEnd = this._Range.endContainer.previousSibling ;

        // Also note that the node that we use for "address base" would change during backtracking.
        var addrStart = this._Range.startContainer ;
        var addrEnd = this._Range.endContainer ;
        while ( curStart && curStart.nodeType == 3 && addrStart.nodeType == 3 )
        {
            bookmark.Start[0] += curStart.length ;
            addrStart = curStart ;
            curStart = curStart.previousSibling ;
        }
        while ( curEnd && curEnd.nodeType == 3 && addrEnd.nodeType == 3 )
        {
            bookmark.End[0] += curEnd.length ;
            addrEnd = curEnd ;
            curEnd = curEnd.previousSibling ;
        }

        // If the object pointed to by the startOffset and endOffset are text nodes, we need
        // to backtrack and add in the text offset to the bookmark addresses.
        if ( addrStart.nodeType == 1 && addrStart.childNodes[bookmark.Start[0]] && addrStart.childNodes[bookmark.Start[0]].nodeType == 3 )
        {
            var curNode = addrStart.childNodes[bookmark.Start[0]] ;
            var offset = 0 ;
            while ( curNode.previousSibling && curNode.previousSibling.nodeType == 3 )
            {
                curNode = curNode.previousSibling ;
                offset += curNode.length ;
            }
            addrStart = curNode ;
            bookmark.Start[0] = offset ;
        }
        if ( addrEnd.nodeType == 1 && addrEnd.childNodes[bookmark.End[0]] && addrEnd.childNodes[bookmark.End[0]].nodeType == 3 )
        {
            var curNode = addrEnd.childNodes[bookmark.End[0]] ;
            var offset = 0 ;
            while ( curNode.previousSibling && curNode.previousSibling.nodeType == 3 )
            {
                curNode = curNode.previousSibling ;
                offset += curNode.length ;
            }
            addrEnd = curNode ;
            bookmark.End[0] = offset ;
        }

        // Then, we record down the precise position of the container nodes
        // by walking up the DOM tree and counting their childNode index
        bookmark.Start = FCKDomTools.GetNodeAddress( addrStart, true ).concat( bookmark.Start ) ;
        bookmark.End = FCKDomTools.GetNodeAddress( addrEnd, true ).concat( bookmark.End ) ;
        return bookmark;
    },

    MoveToBookmark2 : function( bookmark )
    {
        // Reverse the childNode counting algorithm in CreateBookmark2()
        var curStart = FCKDomTools.GetNodeFromAddress( this.Window.document, bookmark.Start.slice( 0, -1 ), true ) ;
        var curEnd = FCKDomTools.GetNodeFromAddress( this.Window.document, bookmark.End.slice( 0, -1 ), true ) ;

        // Generate the W3C Range object and update relevant data
        this.Release( true ) ;
        this._Range = new FCKW3CRange( this.Window.document ) ;
        var startOffset = bookmark.Start[ bookmark.Start.length - 1 ] ;
        var endOffset = bookmark.End[ bookmark.End.length - 1 ] ;
        while ( curStart.nodeType == 3 && startOffset > curStart.length )
        {
            if ( ! curStart.nextSibling || curStart.nextSibling.nodeType != 3 )
                break ;
            startOffset -= curStart.length ;
            curStart = curStart.nextSibling ;
        }
        while ( curEnd.nodeType == 3 && endOffset > curEnd.length )
        {
            if ( ! curEnd.nextSibling || curEnd.nextSibling.nodeType != 3 )
                break ;
            endOffset -= curEnd.length ;
            curEnd = curEnd.nextSibling ;
        }
        this._Range.setStart( curStart, startOffset ) ;
        this._Range.setEnd( curEnd, endOffset ) ;
        this._UpdateElementInfo() ;
    },

    MoveToPosition : function( targetElement, position )
    {
        this.SetStart( targetElement, position ) ;
        this.Collapse( true ) ;
    },

    /*
     * Moves the position of the start boundary of the range to a specific position
     * relatively to a element.
     *        @position:
     *            1 = After Start        <target>^contents</target>
     *            2 = Before End        <target>contents^</target>
     *            3 = Before Start    ^<target>contents</target>
     *            4 = After End        <target>contents</target>^
     */
    SetStart : function( targetElement, position, noInfoUpdate )
    {
        var oRange = this._Range ;
        if ( !oRange )
            oRange = this._Range = this.CreateRange() ;

        switch( position )
        {
            case 1 :        // After Start        <target>^contents</target>
                oRange.setStart( targetElement, 0 ) ;
                break ;

            case 2 :        // Before End        <target>contents^</target>
                oRange.setStart( targetElement, targetElement.childNodes.length ) ;
                break ;

            case 3 :        // Before Start        ^<target>contents</target>
                oRange.setStartBefore( targetElement ) ;
                break ;

            case 4 :        // After End        <target>contents</target>^
                oRange.setStartAfter( targetElement ) ;
        }

        if ( !noInfoUpdate )
            this._UpdateElementInfo() ;
    },

    /*
     * Moves the position of the start boundary of the range to a specific position
     * relatively to a element.
     *        @position:
     *            1 = After Start        <target>^contents</target>
     *            2 = Before End        <target>contents^</target>
     *            3 = Before Start    ^<target>contents</target>
     *            4 = After End        <target>contents</target>^
     */
    SetEnd : function( targetElement, position, noInfoUpdate )
    {
        var oRange = this._Range ;
        if ( !oRange )
            oRange = this._Range = this.CreateRange() ;

        switch( position )
        {
            case 1 :        // After Start        <target>^contents</target>
                oRange.setEnd( targetElement, 0 ) ;
                break ;

            case 2 :        // Before End        <target>contents^</target>
                oRange.setEnd( targetElement, targetElement.childNodes.length ) ;
                break ;

            case 3 :        // Before Start        ^<target>contents</target>
                oRange.setEndBefore( targetElement ) ;
                break ;

            case 4 :        // After End        <target>contents</target>^
                oRange.setEndAfter( targetElement ) ;
        }

        if ( !noInfoUpdate )
            this._UpdateElementInfo() ;
    },

    Expand : function( unit )
    {
        var oNode, oSibling ;

        switch ( unit )
        {
            // Expand the range to include all inline parent elements if we are
            // are in their boundary limits.
            // For example (where [ ] are the range limits):
            //    Before =>        Some <b>[<i>Some sample text]</i></b>.
            //    After =>        Some [<b><i>Some sample text</i></b>].
            case 'inline_elements' :
                // Expand the start boundary.
                if ( this._Range.startOffset == 0 )
                {
                    oNode = this._Range.startContainer ;

                    if ( oNode.nodeType != 1 )
                        oNode = oNode.previousSibling ? null : oNode.parentNode ;

                    if ( oNode )
                    {
                        while ( FCKListsLib.InlineNonEmptyElements[ oNode.nodeName.toLowerCase() ] )
                        {
                            this._Range.setStartBefore( oNode ) ;

                            if ( oNode != oNode.parentNode.firstChild )
                                break ;

                            oNode = oNode.parentNode ;
                        }
                    }
                }

                // Expand the end boundary.
                oNode = this._Range.endContainer ;
                var offset = this._Range.endOffset ;

                if ( ( oNode.nodeType == 3 && offset >= oNode.nodeValue.length ) || ( oNode.nodeType == 1 && offset >= oNode.childNodes.length ) || ( oNode.nodeType != 1 && oNode.nodeType != 3 ) )
                {
                    if ( oNode.nodeType != 1 )
                        oNode = oNode.nextSibling ? null : oNode.parentNode ;

                    if ( oNode )
                    {
                        while ( FCKListsLib.InlineNonEmptyElements[ oNode.nodeName.toLowerCase() ] )
                        {
                            this._Range.setEndAfter( oNode ) ;

                            if ( oNode != oNode.parentNode.lastChild )
                                break ;

                            oNode = oNode.parentNode ;
                        }
                    }
                }

                break ;

            case 'block_contents' :
            case 'list_contents' :
                var boundarySet = FCKListsLib.BlockBoundaries ;
                if ( unit == 'list_contents' || FCKConfig.EnterMode == 'br' )
                    boundarySet = FCKListsLib.ListBoundaries ;

                if ( this.StartBlock && FCKConfig.EnterMode != 'br' && unit == 'block_contents' )
                    this.SetStart( this.StartBlock, 1 ) ;
                else
                {
                    // Get the start node for the current range.
                    oNode = this._Range.startContainer ;

                    // If it is an element, get the node right before of it (in source order).
                    if ( oNode.nodeType == 1 )
                    {
                        var lastNode = oNode.childNodes[ this._Range.startOffset ] ;
                        if ( lastNode )
                            oNode = FCKDomTools.GetPreviousSourceNode( lastNode, true ) ;
                        else
                            oNode = oNode.lastChild || oNode ;
                    }

                    // We must look for the left boundary, relative to the range
                    // start, which is limited by a block element.
                    while ( oNode
                            && ( oNode.nodeType != 1
                                || ( oNode != this.StartBlockLimit
                                    && !boundarySet[ oNode.nodeName.toLowerCase() ] ) ) )
                    {
                        this._Range.setStartBefore( oNode ) ;
                        oNode = oNode.previousSibling || oNode.parentNode ;
                    }
                }

                if ( this.EndBlock && FCKConfig.EnterMode != 'br' && unit == 'block_contents' && this.EndBlock.nodeName.toLowerCase() != 'li' )
                    this.SetEnd( this.EndBlock, 2 ) ;
                else
                {
                    oNode = this._Range.endContainer ;
                    if ( oNode.nodeType == 1 )
                        oNode = oNode.childNodes[ this._Range.endOffset ] || oNode.lastChild ;

                    // We must look for the right boundary, relative to the range
                    // end, which is limited by a block element.
                    while ( oNode
                            && ( oNode.nodeType != 1
                                || ( oNode != this.StartBlockLimit
                                    && !boundarySet[ oNode.nodeName.toLowerCase() ] ) ) )
                    {
                        this._Range.setEndAfter( oNode ) ;
                        oNode = oNode.nextSibling || oNode.parentNode ;
                    }

                    // In EnterMode='br', the end <br> boundary element must
                    // be included in the expanded range.
                    if ( oNode && oNode.nodeName.toLowerCase() == 'br' )
                        this._Range.setEndAfter( oNode ) ;
                }

                this._UpdateElementInfo() ;
        }
    },

    /**
     * Split the block element for the current range. It deletes the contents
     * of the range and splits the block in the collapsed position, resulting
     * in two sucessive blocks. The range is then positioned in the middle of
     * them.
     *
     * It returns and object with the following properties:
     *        - PreviousBlock    : a reference to the block element that preceeds
     *          the range after the split.
     *        - NextBlock : a reference to the block element that follows the
     *          range after the split.
     *        - WasStartOfBlock : a boolean indicating that the range was
     *          originaly at the start of the block.
     *        - WasEndOfBlock : a boolean indicating that the range was originaly
     *          at the end of the block.
     *
     * If the range was originaly at the start of the block, no split will happen
     * and the PreviousBlock value will be null. The same is valid for the
     * NextBlock value if the range was at the end of the block.
     */
    SplitBlock : function( forceBlockTag )
    {
        var blockTag = forceBlockTag || FCKConfig.EnterMode ;

        if ( !this._Range )
            this.MoveToSelection() ;

        // The range boundaries must be in the same "block limit" element.
        if ( this.StartBlockLimit == this.EndBlockLimit )
        {
            // Get the current blocks.
            var eStartBlock        = this.StartBlock ;
            var eEndBlock        = this.EndBlock ;
            var oElementPath    = null ;

            if ( blockTag != 'br' )
            {
                if ( !eStartBlock )
                {
                    eStartBlock = this.FixBlock( true, blockTag ) ;
                    eEndBlock    = this.EndBlock ;    // FixBlock may have fixed the EndBlock too.
                }

                if ( !eEndBlock )
                    eEndBlock = this.FixBlock( false, blockTag ) ;
            }

            // Get the range position.
            var bIsStartOfBlock    = ( eStartBlock != null && this.CheckStartOfBlock() ) ;
            var bIsEndOfBlock    = ( eEndBlock != null && this.CheckEndOfBlock() ) ;

            // Delete the current contents.
            if ( !this.CheckIsEmpty() )
                this.DeleteContents() ;

            if ( eStartBlock && eEndBlock && eStartBlock == eEndBlock )
            {
                if ( bIsEndOfBlock )
                {
                    oElementPath = new FCKElementPath( this.StartContainer ) ;
                    this.MoveToPosition( eEndBlock, 4 ) ;
                    eEndBlock = null ;
                }
                else if ( bIsStartOfBlock )
                {
                    oElementPath = new FCKElementPath( this.StartContainer ) ;
                    this.MoveToPosition( eStartBlock, 3 ) ;
                    eStartBlock = null ;
                }
                else
                {
                    // Extract the contents of the block from the selection point to the end of its contents.
                    this.SetEnd( eStartBlock, 2 ) ;
                    var eDocFrag = this.ExtractContents() ;

                    // Duplicate the block element after it.
                    eEndBlock = eStartBlock.cloneNode( false ) ;
                    eEndBlock.removeAttribute( 'id', false ) ;

                    // Place the extracted contents in the duplicated block.
                    eDocFrag.AppendTo( eEndBlock ) ;

                    FCKDomTools.InsertAfterNode( eStartBlock, eEndBlock ) ;

                    this.MoveToPosition( eStartBlock, 4 ) ;

                    // In Gecko, the last child node must be a bogus <br>.
                    // Note: bogus <br> added under <ul> or <ol> would cause lists to be incorrectly rendered.
                    if ( FCKBrowserInfo.IsGecko &&
                            ! eStartBlock.nodeName.IEquals( ['ul', 'ol'] ) )
                        FCKTools.AppendBogusBr( eStartBlock ) ;
                }
            }

            return {
                PreviousBlock    : eStartBlock,
                NextBlock        : eEndBlock,
                WasStartOfBlock : bIsStartOfBlock,
                WasEndOfBlock    : bIsEndOfBlock,
                ElementPath        : oElementPath
            } ;
        }

        return null ;
    },

    // Transform a block without a block tag in a valid block (orphan text in the body or td, usually).
    FixBlock : function( isStart, blockTag )
    {
        // Bookmark the range so we can restore it later.
        var oBookmark = this.CreateBookmark() ;

        // Collapse the range to the requested ending boundary.
        this.Collapse( isStart ) ;

        // Expands it to the block contents.
        this.Expand( 'block_contents' ) ;

        // Create the fixed block.
        var oFixedBlock = this.Window.document.createElement( blockTag ) ;

        // Move the contents of the temporary range to the fixed block.
        this.ExtractContents().AppendTo( oFixedBlock ) ;
        FCKDomTools.TrimNode( oFixedBlock ) ;

        // If the fixed block is empty (not counting bookmark nodes)
        // Add a <br /> inside to expand it.
        if ( FCKDomTools.CheckIsEmptyElement(oFixedBlock, function( element ) { return element.getAttribute('_fck_bookmark') != 'true' ; } )
                && FCKBrowserInfo.IsGeckoLike )
                FCKTools.AppendBogusBr( oFixedBlock ) ;

        // Insert the fixed block into the DOM.
        this.InsertNode( oFixedBlock ) ;

        // Move the range back to the bookmarked place.
        this.MoveToBookmark( oBookmark ) ;

        return oFixedBlock ;
    },

    Release : function( preserveWindow )
    {
        if ( !preserveWindow )
            this.Window = null ;

        this.StartNode = null ;
        this.StartContainer = null ;
        this.StartBlock = null ;
        this.StartBlockLimit = null ;
        this.EndNode = null ;
        this.EndContainer = null ;
        this.EndBlock = null ;
        this.EndBlockLimit = null ;
        this._Range = null ;
        this._Cache = null ;
    },

    CheckHasRange : function()
    {
        return !!this._Range ;
    },

    GetTouchedStartNode : function()
    {
        var range = this._Range ;
        var container = range.startContainer ;

        if ( range.collapsed || container.nodeType != 1 )
            return container ;

        return container.childNodes[ range.startOffset ] || container ;
    },

    GetTouchedEndNode : function()
    {
        var range = this._Range ;
        var container = range.endContainer ;

        if ( range.collapsed || container.nodeType != 1 )
            return container ;

        return container.childNodes[ range.endOffset - 1 ] || container ;
    }
} ;
